// Written in the D Programming Language
/**
 * The 17th lesson in the <a href="http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=17">NeHe tutorial series</a>.
 * Originally written by Giuseppe D'Agata.
 *
 * Authors: Giuseppe D'Agata
 *          Jeff Molofee
 *          Olli Aalto
 */
module lesson17;

import derelict.opengl.gl;
import derelict.opengl.glu;
import derelict.sdl.sdl;

import tango.stdc.stringz;
import Math = tango.math.Math;

const WINDOW_WIDTH = 640;
const WINDOW_HEIGHT = 480;

/// The window title
const char[]
	WINDOW_TITLE = "NeHe & Giuseppe D'Agata's 2D Font Tutorial (D version)";

/// The main loop flag
bool running = true;

/// Base Display List For The Font
GLuint base;
/// Storage For Our Font Textures
GLuint[2] textures;
/// Generic Loop Variable
GLuint loop;

/// 1st Counter Used To Move Text & For Coloring
GLfloat cnt1 = 0.0f;
/// 2nd Counter Used To Move Text & For Coloring
GLfloat cnt2 = 0.0f;

/**
 * Module constructor. Here we load the GL, GLU and SDL shared libraries,
 * and the initialize SDL.
 */
static this()
{
	DerelictGL.load();
	DerelictGLU.load();
	DerelictSDL.load();

	if (SDL_Init(SDL_INIT_VIDEO) < 0)
	{
		throw new Exception("Failed to initialize SDL: " ~ getSDLError());
	}
}

/**
 * Module destructor. SDL_Quit must be called somewhere, and as we initialized
 * it in the module constructor so the module destructor should be a suitable
 * place.
 */
static ~this()
{
	SDL_Quit();
}

/**
 * The main function. This is where the fun begins. The first order of business
 * is the check the command line arguments if the user wanted to start in
 * fullscreen mode. Then the window is created and OpenGL is initialized with
 * basic settings. Finally the the function starts the main loop which will live
 * for the duration of the application.
 *
 * Params:
 *      args = the command line arguments
 */
void main(char[][] args)
{
	bool fullScreen = false;

	if (args.length > 1)
	{
		fullScreen = args[1] == "-fullscreen";
	}

	createGLWindow(WINDOW_TITLE, WINDOW_WIDTH, WINDOW_HEIGHT, 16, fullScreen);
	initGL();

	while (running)
	{
		processEvents();

		drawGLScene();

		SDL_GL_SwapBuffers();
		SDL_Delay(10);
	}

	killFont();
}

/**
 * Process all the pending events.
 */
void processEvents()
{
	SDL_Event event;
	while (SDL_PollEvent(&event))
	{
		switch (event.type)
		{
			case SDL_KEYUP:
				keyReleased(event.key.keysym.sym);
				break;
			case SDL_QUIT:
				running = false;
				break;
			default:
				break;
		}
	}
}

/**
 * Process a key released event.
 */
void keyReleased(int key)
{
	switch (key)
	{
		case SDLK_ESCAPE:
			running = false;
			break;
		default:
			break;
	}
}

/**
 * function to load in bitmap as a GL texture.
 */
void loadGLTextures()
{
	// Create storage space for the texture
	SDL_Surface* fontImage;
	SDL_Surface* bumpImage;

	// Load The Bitmap, Check For Errors, If Bitmap's Not Found Quit
	if ((fontImage = SDL_LoadBMP("data/Font.bmp")) !is null && (bumpImage = SDL_LoadBMP(
		"data/Bumps.bmp")) !is null)
	{
		// Free the surfaces when we exit the scope
		scope(exit)
			SDL_FreeSurface(fontImage);
		scope(exit)
			SDL_FreeSurface(bumpImage);

		glGenTextures(2, &textures[0]);

		glBindTexture(GL_TEXTURE_2D, textures[0]);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexImage2D(GL_TEXTURE_2D, 0, 3, fontImage.w, fontImage.h, 0, GL_BGR,
			GL_UNSIGNED_BYTE, fontImage.pixels);

		glBindTexture(GL_TEXTURE_2D, textures[1]);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexImage2D(GL_TEXTURE_2D, 0, 3, bumpImage.w, bumpImage.h, 0, GL_BGR,
			GL_UNSIGNED_BYTE, bumpImage.pixels);

		return;
	}
	throw new Exception("Failed to load textures.");
}

/**
 * Build Our Font Display List.
 */
void buildFont()
{
	// Holds Our X Character Coord
	float cx = 0.0f;
	// Holds Our Y Character Coord
	float cy = 0.0f;

	// Creating 256 Display Lists
	base = glGenLists(256);
	// Select Our Font Texture
	glBindTexture(GL_TEXTURE_2D, textures[0]);
	// Loop Through All 256 Lists
	for (int loop = 0; loop < 256; loop++)
	{
		// X Position Of Current Character
		cx = cast(float) (loop % 16) / 16.0f;
		// Y Position Of Current Character
		cy = cast(float) (loop / 16) / 16.0f;

		// Start Building A List
		glNewList(base + loop, GL_COMPILE);
		{
			// Use A Quad For Each Character
			glBegin(GL_QUADS);
			{
				// Texture Coord (Bottom Left)
				glTexCoord2f(cx, 1 - cy - 0.0625f);
				// Vertex Coord (Bottom Left)
				glVertex2i(0, 0);
				// Texture Coord (Bottom Right)
				glTexCoord2f(cx + 0.0625f, 1 - cy - 0.0625f);
				// Vertex Coord (Bottom Right)
				glVertex2i(16, 0);
				// Texture Coord (Top Right)
				glTexCoord2f(cx + 0.0625f, 1 - cy);
				// Vertex Coord (Top Right)
				glVertex2i(16, 16);
				// Texture Coord (Top Left)
				glTexCoord2f(cx, 1 - cy);
				// Vertex Coord (Top Left)
				glVertex2i(0, 16);
			}
			// Done Building Our Quad (Character)
			glEnd();
			// Move To The Right Of The Character
			glTranslated(10, 0, 0);
		}
		// Done Building The Display List
		glEndList();
	}
}

/**
 * Delete The Font From Memory
 */
void killFont()
{
	// Delete All 256 Display Lists
	glDeleteLists(base, 256);
}

/**
 * This Is Where The Printing Happens.
 * 
 * Params:
 *     x = the x coordinate of the text
 *     y = the y coordinate of the text
 *     string = the text to be printed
 *     set = font set
 */
void glPrint(GLint x, GLint y, char[] string, int set)
{
	if (set > 1)
	{
		set = 1;
	}
	// Select Our Font Texture
	glBindTexture(GL_TEXTURE_2D, textures[0]);
	// Disables Depth Testing
	glDisable(GL_DEPTH_TEST);
	// Select The Projection Matrix
	glMatrixMode(GL_PROJECTION);
	// Store The Projection Matrix
	glPushMatrix();
	// Reset The Projection Matrix
	glLoadIdentity();
	// Set Up An Ortho Screen
	glOrtho(0, WINDOW_WIDTH, 0, WINDOW_HEIGHT, -1, 1);
	// Select The Modelview Matrix
	glMatrixMode(GL_MODELVIEW);
	// Store The Modelview Matrix
	glPushMatrix();
	// Reset The Modelview Matrix
	glLoadIdentity();
	// Position The Text (0,0 - Bottom Left)
	glTranslated(x, y, 0);
	// Choose The Font Set (0 or 1)
	glListBase(base - 32 + (128 * set));
	// Write The Text To The Screen
	glCallLists(string.length, GL_UNSIGNED_BYTE, toStringz(string));
	// Select The Projection Matrix
	glMatrixMode(GL_PROJECTION);
	// Restore The Old Projection Matrix
	glPopMatrix();
	// Select The Modelview Matrix
	glMatrixMode(GL_MODELVIEW);
	// Restore The Old Projection Matrix
	glPopMatrix();
	// Enables Depth Testing
	glEnable(GL_DEPTH_TEST);
}

/**
 * Resize And Initialize The GL Window.
 */
void resizeGLScene(GLsizei width, GLsizei height)
{
	if (height == 0)
	{
		height = 1;
	}
	// Reset The Current Viewport
	glViewport(0, 0, width, height);

	// Select The Projection Matrix
	glMatrixMode(GL_PROJECTION);
	// Reset The Projection Matrix
	glLoadIdentity();

	// Calculate The Aspect Ratio Of The Window
	gluPerspective(45.0f, cast(GLfloat) width / cast(GLfloat) height, 0.1f,
		100.0f);

	// Select The Modelview Matrix
	glMatrixMode(GL_MODELVIEW);
	// Reset The Modelview Matrix
	glLoadIdentity();
}

/**
 * All Setup For OpenGL Goes Here.
 */
void initGL()
{
	loadGLTextures();
	buildFont();

	// Clear The Background Color To Black
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
	// Enables Clearing Of The Depth Buffer
	glClearDepth(1.0);
	// The Type Of Depth Test To Do
	glDepthFunc(GL_LEQUAL);
	// Select The Type Of Blending
	glBlendFunc(GL_SRC_ALPHA, GL_ONE);
	// Enables Smooth Color Shading
	glShadeModel(GL_SMOOTH);
	// Enable 2D Texture Mapping
	glEnable(GL_TEXTURE_2D);

	// Change to texture matrix and flip and rotate the texture
	glMatrixMode(GL_TEXTURE);
	glRotatef(180.0f, 0.0f, 0.0f, 1.0f);
	glScalef(-1.0f, 1.0f, 1.0f);
	// Back to normal
	glMatrixMode(GL_MODELVIEW);
}

/**
 * Here's Where We Do All The Drawing.
 */
void drawGLScene()
{
	// Clear The Screen And The Depth Buffer
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	// Reset The Modelview Matrix
	glLoadIdentity();
	// Select Our Second Texture
	glBindTexture(GL_TEXTURE_2D, textures[1]);
	// Move Into The Screen 5 Units
	glTranslatef(0.0f, 0.0f, -5.0f);
	// Rotate On The Z Axis 45 Degrees (Clockwise)
	glRotatef(45.0f, 0.0f, 0.0f, 1.0f);
	// Rotate On The X & Y Axis By cnt1 (Left To Right)
	glRotatef(cnt1 * 30.0f, 1.0f, 1.0f, 0.0f);
	// Disable Blending Before We Draw In 3D
	glDisable(GL_BLEND);
	// Bright White
	glColor3f(1.0f, 1.0f, 1.0f);

	// Draw Our First Texture Mapped Quad
	glBegin(GL_QUADS);
	{
		glTexCoord2d(0.0f, 0.0f);
		glVertex2f(-1.0f, 1.0f);
		glTexCoord2d(1.0f, 0.0f);
		glVertex2f(1.0f, 1.0f);
		glTexCoord2d(1.0f, 1.0f);
		glVertex2f(1.0f, -1.0f);
		glTexCoord2d(0.0f, 1.0f);
		glVertex2f(-1.0f, -1.0f);
	}
	glEnd();

	// Rotate On The X & Y Axis By 90 Degrees (Left To Right)
	glRotatef(90.0f, 1.0f, 1.0f, 0.0f);

	// Draw Our Second Texture Mapped Quad
	glBegin(GL_QUADS);
	{
		glTexCoord2d(0.0f, 0.0f);
		glVertex2f(-1.0f, 1.0f);
		glTexCoord2d(1.0f, 0.0f);
		glVertex2f(1.0f, 1.0f);
		glTexCoord2d(1.0f, 1.0f);
		glVertex2f(1.0f, -1.0f);
		glTexCoord2d(0.0f, 1.0f);
		glVertex2f(-1.0f, -1.0f);
	}
	glEnd();

	// Enable Blending
	glEnable(GL_BLEND);

	// Reset The View
	glLoadIdentity();
	// Pulsing Colors Based On Text Position
	glColor3f(1.0f * cast(float) (Math.cos(cnt1)),
		1.0f * cast(float) (Math.sin(cnt2)),
		1.0f - 0.5f * cast(float) (Math.cos(cnt1 + cnt2)));
	// Print GL Text To The Screen
	glPrint(cast(int) ((280 + 250 * Math.cos(cnt1))),
		cast(int) (235 + 200 * Math.sin(cnt2)), "NeHe", 0);

	glColor3f(1.0f * cast(float) (Math.sin(cnt2)),
		1.0f - 0.5f * cast(float) (Math.cos(cnt1 + cnt2)),
		1.0f * cast(float) (Math.cos(cnt1)));
	// Print GL Text To The Screen
	glPrint(cast(int) ((280 + 230 * Math.cos(cnt2))),
		cast(int) (235 + 200 * Math.sin(cnt1)), "OpenGL", 1);

	// Set Color To Blue
	glColor3f(0.0f, 0.0f, 1.0f);
	glPrint(cast(int) (240 + 200 * Math.cos((cnt2 + cnt1) / 5)), 2,
		"Giuseppe D'Agata", 0);

	// Set Color To White
	glColor3f(1.0f, 1.0f, 1.0f);
	glPrint(cast(int) (242 + 200 * Math.cos((cnt2 + cnt1) / 5)), 2,
		"Giuseppe D'Agata", 0);

	// Increase The First Counter
	cnt1 += 0.01f;
	// Increase The Second Counter}
	cnt2 += 0.0081f;
}

void createGLWindow(char[] title, int width, int height, int bits,
	bool fullScreen)
{
	// Set the OpenGL attributes
	SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6);
	SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

	// Set the window title
	SDL_WM_SetCaption(toStringz(title), null);

	/* Note the SDL_DOUBLEBUF flag is not required to enable double
	 * buffering when setting an OpenGL video mode.
	 * Double buffering is enabled or disabled using the
	 * SDL_GL_DOUBLEBUFFER attribute.
	 */
	int mode = SDL_OPENGL;
	if (fullScreen)
	{
		mode |= SDL_FULLSCREEN;
	}
	// Now open a SDL OpenGL window with the given parameters
	if (SDL_SetVideoMode(width, height, bits, mode) is null)
	{
		throw new Exception("Failed to open OpenGL window: " ~ getSDLError());
	}

	resizeGLScene(width, height);
}

/**
 * Get the SDL error as a D string.
 *
 * Returns: A D string containing the current SDL error.
 */
char[] getSDLError()
{
	return fromStringz(SDL_GetError());
}
